package com.atguigu.zk.distributedlock;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * 手动实现
 * 利用zk来管理分布式锁
 */
public class DistributedLock {


    private String connectString = "lc01:2181,lc02:2181,lc03:2181";
    private int sessionTimeOut = 2000;
    private ZooKeeper zooKeeper = null;
    private String lastNodePath = null; // 当前节点的前一个节点的完整路径，需要被监听
    private String currentNodePath; // 当前线程生成的节点路径


    // 锁存器，作用 1：某一线程在开始运行前等待n个线程执行完毕。 2、实现多个线程开始执行任务的最大并行性。
    private CountDownLatch connectLatch = new CountDownLatch(1); // 开启一个额外线程来阻塞主线程，直至释放。用于等待连接
    private CountDownLatch waitWatchLatch = new CountDownLatch(1); // 开启一个额外线程来阻塞主线程，直至释放。用于等待监听器被触发并执行结束


    public DistributedLock() throws IOException, InterruptedException, KeeperException {
        zooKeeper = new ZooKeeper(connectString, sessionTimeOut, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
                // 如果当前监听到的事件的状态为同步连接状态，说明已经连接上服务器，可以放开 connectLatch 线程，让主程序继续执行下去。
                if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
                    System.out.println(String.format("监听器--观察到连接远程服务状态为同步，连接成功"));
                    connectLatch.countDown(); // 递减锁存器的计数，如果计数到达零，则释放所有等待的线程。
                }
                // 如果当前监听到的事件的状态为节点删除，且是上个节点被删除，这时可以释放掉 waitWatchLatch 线程，让等待着上个节点消失的主线程继续执行下去。
                if (watchedEvent.getType() == Event.EventType.NodeDeleted && watchedEvent.getPath().equals(lastNodePath)) {
                    System.out.println(String.format("监听器--观察到前一个节点%s被删除", lastNodePath));
                    waitWatchLatch.countDown();
                }

            }
        });

        // zk连接成功后，才会继续往下走。起到阻塞的作用
        connectLatch.await(); // 使当前线程在锁存器倒计数至零之前一直等待，除非线程被中断或超出了指定的等待时间。

        // 判断locks信息的根节点是否存在。
        Stat locks = zooKeeper.exists("/locks", false);
        if (locks == null) {
            // 不存在就创建
            zooKeeper.create("/locks", "locks_val".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }


    }

    // 向zk中添加锁数据，并等待该锁可用
    public void addLock() {
        try {
            // 创建当前访问对应的临时带序号节点
            currentNodePath = zooKeeper.create("/locks/" + "seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            // 去除前面目录后的里层节点名称 例如： seq-000000000  seq-000000001
            String currentNodeName = currentNodePath.replace("/locks/", "");
            // 判断当前创建的节点是否为最小的序号节点
            // 如果是，说明已经可以拿到锁，可以操作资源
            // 如果不是，说明需要监听它前面的一个节点
            List<String> children = zooKeeper.getChildren("/locks", false); // 注意, 没有必要监听"/locks"的子节点的变化情况
            if (children.size() == 1) {
                // 只有一个节点，说明可以直接获取锁
                System.out.println(String.format("%s对应的访问者，可以获取资源了",currentNodePath));
                return;
            } else {
                // 对根节点下的所有临时顺序节点进行从小到大排序
                Collections.sort(children);
                // 当前节点的位置
                int index = children.indexOf(currentNodeName);
                if (index == -1) {
                    System.out.println(String.format("节点%s不在目标集合中，数据异常", currentNodeName));
                } else if (index == 0) {
                    // 只有一个节点，说明可以直接获取锁
                    System.out.println(String.format("%s对应的访问者，可以获取资源了",currentNodePath));
                    return;
                } else {
                    // 需要监听前一个节点的变化
                    lastNodePath = "/locks/" + children.get(index - 1);
                    // 通过get指令，监听上个节点的变化
                    zooKeeper.getData(lastNodePath, true, null);

                    // 进入等待锁状态，等待监听器被触发并执行结束，再往下执行
                    waitWatchLatch.await(); // 等待 waitWatchLatch 被释放，说明上个节点被删除
                    System.out.println(String.format("%s对应的访问者，可以获取资源了",currentNodePath));
                    return;

                }
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
        }


    }


    // 删除zk中的当前访问者生成的所信息
    public void removeLock() {
        try {
            zooKeeper.delete(currentNodePath,-1);
            System.out.println(String.format("删除%s完成",currentNodePath));
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        } finally {
        }

    }
}
